package net.ib.mtalk.network.pipeline;

import java.net.ConnectException;
import java.nio.channels.UnresolvedAddressException;

import net.ib.mtalk.global.GlobalApplication;
import net.ib.mtalk.network.NetFlowControl;
import net.ib.mtalk.network.TcpClient;
import net.ib.mtalk.network.TcpEnvelope;
import net.ib.mtalk.util.MTalkLogWriter;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelFutureProgressListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.ssl.SslHandler;

public class TcpClientChannelHandler extends NetClientChannelHandler {

	private ClientBootstrap bootstrap = null;
	private String ownerClass;

	private Channel currentChannel = null;
	private boolean isCurrentChannel(Channel channel) {
		if (currentChannel == null || channel == null) {
			return false;
		}
		if (currentChannel.getId() == channel.getId()) {
			return true;
		}
		return false;
	}

	public TcpClientChannelHandler(ClientBootstrap bootstrap, String ownerClass) {
		this.bootstrap = bootstrap;
		this.ownerClass = ownerClass;
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".exceptionCaught() - STT");

		Throwable cause = e.getCause();
		if (cause instanceof ConnectException) {
			MTalkLogWriter.v("TcpClientChannelHandler.exceptionCaught(): Failed to connect: " + cause.getMessage());
			if (TcpClient.class.getSimpleName().equals(ownerClass)) {
				NetFlowControl.resetSessionAddress();
			}
		} else if (cause instanceof UnresolvedAddressException) {
			MTalkLogWriter.v("TcpClientChannelHandler.exceptionCaught(): Failed to connect: " + cause.getMessage());
			if (TcpClient.class.getSimpleName().equals(ownerClass)) {
				NetFlowControl.resetSessionAddress();
			}
		} else {
			MTalkLogWriter.e(e.getCause());
			if (TcpClient.class.getSimpleName().equals(ownerClass)) {
				NetFlowControl.resetSessionAddress();
			}
		}
		ctx.getChannel().close();

		MTalkLogWriter.v(this.getClass().getSimpleName()+".exceptionCaught() - END");
	}

	public ChannelFuture closeChannel() {
		ChannelFuture reVal = null;
		if (currentChannel != null) {
			reVal = currentChannel.close().awaitUninterruptibly();
		}

		return reVal;
	}

	/**
	 * If There is only one Channel, return true. 
	 * @return validate result.
	 */
	public boolean validateChannel() {
		if (currentChannel != null && currentChannel.isConnected()) {
			return true;
		}
		MTalkLogWriter.i("TcpClientChannelHandler.validateChannels(): Channels validation fail.");
		return false;
	}

	@Override
	public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".channelOpen() - STT");

		closeChannel();
		currentChannel = e.getChannel();
		MTalkLogWriter.v("TcpClientChannelHandler.channelOpen(): Opened Channel ID: " + e.getChannel().getId());

		MTalkLogWriter.v(this.getClass().getSimpleName()+".channelOpen() - END");
	}

	@Override
	public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		super.channelConnected(ctx, e);
		MTalkLogWriter.d("TcpClientChannelHandler.channelOpen(): Connected Channel ID: " + e.getChannel().getId());
		// SSL handshake & getting Auth
		SslHandler sslHandler = ctx.getPipeline().get(SslHandler.class);
		if (sslHandler == null) {
			MTalkLogWriter.v("SSL Layer not found");
			if (TcpClient.class.getSimpleName().equals(ownerClass)) {
				NetFlowControl.checkSessionAddress();
				NetFlowControl.tryGetAuth();
			}
		} else {
			MTalkLogWriter.v("SSL Layer found");
			ChannelFuture channelFuture = sslHandler.handshake();
			channelFuture.addListener(new ChannelFutureListener() {
				@Override
				public void operationComplete(ChannelFuture future) throws Exception {
					if (future.isSuccess()) {
						MTalkLogWriter.d("SSL handshake OK");
						if (TcpClient.class.getSimpleName().equals(ownerClass)) {
							NetFlowControl.checkSessionAddress();
							NetFlowControl.tryGetAuth();
						}
					} else {
						throw new Exception("SSL handshake Fail");
					}
				}
			});
			throw new Exception("SSL handshake Timeout");
		}
	}

	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".channelClosed() - STT");
		super.channelClosed(ctx, e);
		MTalkLogWriter.d("TcpClientChannelHandler.channelClosed(): Closed Channel ID: " + e.getChannel().getId());

		if (isCurrentChannel(e.getChannel())) {
			if (TcpClient.class.getSimpleName().equals(ownerClass)) {
				NetFlowControl.removeAuth();
			}
			currentChannel = null;

			try {
				TcpEnvelopeBicoder tcpEnvelopeBicoder = bootstrap.getPipeline().get(TcpEnvelopeBicoder.class);
				if (tcpEnvelopeBicoder != null) {
					tcpEnvelopeBicoder.kickHistory();
				}
			} catch (Exception exc) {
				MTalkLogWriter.e(exc);
			}
		}

		// For abnormal closing.(Ex. Cut the line...)
		Channels.fireChannelClosed(ctx);
		Channels.fireChannelDisconnected(ctx);

		MTalkLogWriter.v(this.getClass().getSimpleName()+".channelClosed() - END");
	}

	/**
	 * Programmer has to connect to server before write an Object to channel.
	 * @param tcpEnvelope A TcpEnvelope for sending to server.
	 * @return RequestId of revelope. If it is a request, ID will be generated newly. '-1' will return if write failed.
	 */
	public int writeChannel(TcpEnvelope tcpEnvelope) {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".writeRevelope() - STT");

		int result = -1;
		if(! validateChannel()) {
			MTalkLogWriter.v(this.getClass().getSimpleName()+".writeRevelope() - END with no work");
			return result;
		}

		if (tcpEnvelope.getType() == TcpEnvelope.REQUEST) {	// 메시지가 질문이라면
			if (TcpEnvelope.NONE == tcpEnvelope.getRequestId()) {	// RequestId가 생성되어 있지 않다면
				tcpEnvelope.setRequestId(TcpEnvelope.getNewRequestId());	// RequestId를 세팅
			}
		}
		result = tcpEnvelope.getRequestId();	// RequestId를 읽어보자

		ChannelFuture writeFuture = currentChannel.write(tcpEnvelope);//allChannels.write(tcpEnvelope);
		writeFuture.addListener(new ChannelFutureProgressListener() {	// 전송 상태를 알고싶다.

			@Override
			public void operationComplete(ChannelFuture future) throws Exception {
				MTalkLogWriter.v("ChannelFutureProgressListener: Channel write CMPL");
			}

			@Override
			public void operationProgressed(ChannelFuture future, long amount, long current, long total) throws Exception {
				MTalkLogWriter.v("ChannelFutureProgressListener: Channel write "+amount+" ("+current+"/"+total+")");
			}
		});
		if (TcpClient.class.getSimpleName().equals(ownerClass)) {
			NetFlowControl.trafficControl(GlobalApplication.getContext());
		}

		MTalkLogWriter.v(this.getClass().getSimpleName()+".writeRevelope() - END");
		return result;
	}

	@Override
	public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".writeRequested() - STT");

		super.writeRequested(ctx, e);	// 보내기

		MTalkLogWriter.v(this.getClass().getSimpleName()+".writeRequested() - END");
	}

	/**
	 * 받은 {@link TcpEnvelope}에 대한 처리를 수행한다.
	 *
	 * @see org.jboss.netty.channel.SimpleChannelHandler#messageReceived(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.MessageEvent)
	 */
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
		MTalkLogWriter.v(this.getClass().getSimpleName()+".messageReceived() - STT");

		TcpEnvelope tcpEnvelope = (TcpEnvelope) e.getMessage();
		if (tcpEnvelope == null) {
			// Revelope을 만들 수 없었다면?

			/* 서버로부터 질문이 온 건지 응답이 온 건지 알 수 없었던 경우일 수 있다.
			 * 이 경우 서버에 응답을 할지 말지 결정할 수조차 없다. 아무 것도 하지 말자.
			 * 
			 * 또는 서버로부터 응답이 왔는데 대응되는 질문을 찾을 수 없는 경우일 수도 있다.
			 * 서버에 응답할 필요는 없다. 다만 클라이언트의 질문 기록이 잘못 관리되고 있는 것은 아닌지 검토해야 한다.
			 * 
			 * 이것은 컴파일타임 문제이며 런타임에 대처할 일은 없다.
			 * */

			MTalkLogWriter.v(this.getClass().getSimpleName()+".messageReceived() : May not reach here.");
			MTalkLogWriter.v(this.getClass().getSimpleName()+".messageReceived() - END");
			return;
		}

		if (TcpClient.class.getSimpleName().equals(ownerClass)) {
			NetFlowControl.handleResponse(tcpEnvelope);
			NetFlowControl.trafficControl(GlobalApplication.getContext());
		}

		if (tcpEnvelope.getCallback() != null) {
			// 콜백이 null이 아니었다면?
			// 콜백 함수가 불렸을 것이다. 콜백 객체가 알아서 했겠지. ~('ㅅ'

			// do nothing
		} else {
			// 콜백이 없다면?

			/* 서버가 질문을 했고 Revelope은 만들었지만(=대응하는 질문을 찾았다) 콜백을 지정할 수 없었다면,
			 * 메소드를 판정하지 못한 경우이다.
			 * 이 경우 응답 메시지 객체 타입을 알 수 없으니 아무것도 할 수 없다.
			 *
			 * 서버가 응답을 했고 Revelope을 만들었는데(=대응하는 질문을 찾았다) 콜백을 지정할 수 없었다면,
			 * 애초에 질문을 할 때 콜백을 지정하지 않은 경우이다.
			 * 클라이언트가 그것을 의도했다면 그런대로 내버려 두자.
			 * */

			// do nothing
		}

		MTalkLogWriter.v(this.getClass().getSimpleName()+".messageReceived() - END");
	}

	/**
	 * 유통기한이 지난 전송 기록들을 치워버린다. 해당 기록들은 모두 에러 콜백이 호출될 것이다.
	 */
	public void expiredCollector() {
		try {
			TcpEnvelopeBicoder tcpEnvelopeBicoder = bootstrap.getPipeline().get(TcpEnvelopeBicoder.class);
			if (tcpEnvelopeBicoder != null) {
				tcpEnvelopeBicoder.expiredCollector();
			}
		} catch (Exception e) {
			MTalkLogWriter.e(e);
		}
	}

}
